//! Finds all .sql files in `cases/in/` and creates corresponding
//! entries in src/cases.rs as native Rust test runner tests

use std::ffi::OsStr;
use std::path::{Path, PathBuf};

type Error = Box<dyn std::error::Error>;
type Result<T, E = Error> = std::result::Result<T, E>;

fn main() -> Result<()> {
    // Ignores all args and finds relative paths based on PWD and the command

    // example: query_tests/generate/target/debug/generate
    let current_exe = std::env::current_exe()?;

    // walk up parent tree looking for query_tests
    let mut query_tests = current_exe.clone();
    let needle = OsStr::new("query_tests");
    loop {
        if query_tests.file_name() == Some(&needle) {
            break;
        }
        if !query_tests.pop() {
            panic!("Can not find 'query_tests' in the path: {:?}", current_exe);
        }
    }

    // crate root
    let cases = query_tests.join("cases").join("in");

    let sql_files = find_sql_files(&cases);

    // Now create the generated sql file
    let output_content = make_cases_rs(&sql_files).join("\n");
    let output_file = query_tests.join("src").join("cases.rs");
    write_if_changed(&output_file, &output_content);

    println!("Done");
    Ok(())
}

/// Returns a sorted list of all files named `.sql` in the specified root directory
fn find_sql_files(root: &Path) -> Vec<PathBuf> {
    let mut sqls: Vec<PathBuf> = root
        .read_dir()
        .map_err(|e| format!("can not read root: {:?}: {}", root, e))
        .unwrap()
        .map(|dir_ent| {
            let dir_ent = dir_ent
                .map_err(|e| format!("can not read directory entry: {}", e))
                .unwrap();
            dir_ent.path()
        })
        .filter(|p| p.extension() == Some(std::ffi::OsStr::new("sql")))
        .collect();

    sqls.sort();
    sqls
}

/// writes out what will be the rust test file that lists out .sqls
fn make_cases_rs(sqls: &[PathBuf]) -> Vec<String> {
    let mut output_lines: Vec<String> = vec![r#"
//! This file is auto generated by build.rs
//! Do not edit manually --> will result in sadness
use std::path::Path;
use crate::runner::Runner;"#
        .into()];

    for sql in sqls {
        let file_name = sql.file_name().expect("a name").to_string_lossy();

        let test_name = file_name.replace(".", "_");

        output_lines.push(format!(
            r#"
#[tokio::test]
// Tests from {:?},
async fn test_cases_{}() {{
    let input_path = Path::new("cases").join("in").join("{}");
    let mut runner = Runner::new();
    runner
        .run(input_path)
        .await
        .expect("test failed");
    runner
        .flush()
        .expect("flush worked");
}}"#,
            file_name, test_name, file_name
        ));
    }

    output_lines
}

/// Write content to file if it is different to the current content.
///
/// This prevents us from touching the file and modifying the `mtime` so that Cargo will re-compile the output `.rs`
/// file every time. Also see [`rust-lang/cargo#6529`](https://github.com/rust-lang/cargo/issues/6529).
fn write_if_changed(path: &Path, content: &str) {
    let changed = if !path.exists() {
        true
    } else {
        match std::fs::read_to_string(path) {
            Ok(old_content) => old_content != content,
            Err(_) => true,
        }
    };

    if changed {
        println!("Writing changes to {}", path.display());

        std::fs::write(path, content)
            .map_err(|e| format!("Error writing to {:?}: {}", path, e))
            .unwrap();
    }
}
